package objects.base;

import java.util.HashSet;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.state.StateBasedGame;

import util.collision.SpaceHash;
import util.level.Room;

public class Collidable extends Movable
{
	// Booleans that can be checked to see if an object has collided in a certain direction.
	// Index: (0 -> Negative, 1 -> Positive) direction.
	protected boolean horizStop[] = {false, false};
	protected boolean vertStop[] = {false, false};
	
	// Determines what kinds of objects can be pushed out of the way.
	// Ties are broken by having both stop.
	protected int pushPriority = 2;
	
	// How granular movements can be.
	private static float GRANULARITY = 256f;
	
	// This hash set gets used to prevent an object from checking collision against objects checked against.
	private HashSet<Collidable> alreadyChecked = new HashSet<Collidable>();
	
	// A variable that stores the object that was collided with, if it hit an object during its own movement.
	protected Collidable objectStopX = null;
	protected Collidable objectStopY = null;
	// This stores the last object the object collided with in general, regardless if you care about it.
	protected Collidable lastObjectStop = null;
	
	/**
	 * Initializes a collidable object of given size at the given position.
	 * 
	 * @param initX - The initial X position.
	 * @param initY - The initial Y position.
	 * @param initWidth - The initial width of the object.
	 * @param initHeight - The initial height of the object.
	 */
	public Collidable(float initX, float initY, float initWidth, float initHeight)
	{
		// Call super constructor
		super(initX, initY, initWidth, initHeight);
	}
	
	
	public void creation(GameContainer gc,StateBasedGame sbg,Room room)
	{
		// Call method from superclass
		super.creation(gc,sbg,room);
		// Add to hash
		addHash(room);
	}

	/**
	 * Removes the object from the collision hash before destroying it.
	 * 
	 * @param gc - the current game container.
	 * @param game - the current game.
	 */
	public void destroy(GameContainer gc,StateBasedGame sbg,Room room)
	{
		// Destruct parent
		super.destroy(gc,sbg,room);
		// Remove from hash
		removeHash(room);
	}
	
	/**
	 * Wraps motion with removing and adding the object from
	 * the collision hash before and after it moves respectively.
	 * 
	 * @param gc {@inheritDoc}
	 * @param game {@inheritDoc}
	 */
	public void step(GameContainer gc,StateBasedGame sbg,Room room)
	{
		// Move the object
		super.step(gc,sbg,room);
	}
	
	/**
	 * Returns whether or not the object has inertia.
	 * 
	 * @return A boolean where TRUE -> Can be carried, FALSE -> Cannot.
	 */
	public boolean hasInertia()
	{
		return hasInertia;
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * @param moveX - {@inheritDoc}
	 * @param moveY - {@inheritDoc}
	 */
	public void doMotion(Room room,float moveX, float moveY)
	{
		// Remove the object from the hash
		removeHash(room);	
		// Reset all the collision flags
		if (moveX != 0f)
		{
			horizStop[0] = false;
			horizStop[1] = false;
		}
		if (moveY != 0f)
		{
			vertStop[0] = false;
			vertStop[1] = false;
		}
		objectStopX = null;
		objectStopY = null;
		// Actually do the movement
		super.doMotion(room,moveX, moveY);
		// Perform collision behavior on objects hit
		tryCollision(room,objectStopX);
		tryCollision(room,objectStopY);
		// Add the object to the hash
		addHash(room);
	}
	
	/**
	 * Wraps performing collision against an object with various checks.
	 * 
	 * @param otherObj - The object to do collision on.
	 */
	private void tryCollision(Room room,Collidable otherObj)
	{
		// If the object given is nothing, do nothing
		if (otherObj == null) return;
		// If the object just collided with you, don't do anything to it
		if (otherObj.objectStopX == this || otherObj.objectStopY == this) return;
		// Otherwise, perform both of their collision functions
		collision(room,otherObj);
		otherObj.collision(room,this);
	}
	
	/**
	 * {@inheritDoc}
	 */
	protected float carryInertiaX(float moveX)
	{
		// Nothing happens with blocks you are horizontally colliding with yet
		// If the object carrying you (above/below) could push you
		if (objectStopY != null && pushPriority < objectStopY.pushPriority)
		{
			// Let it carry you horizontally
			return moveX+objectStopY.totalXSpeed;
		}
		return moveX;
	}
	
	/**
	 * {@inheritDoc}
	 */
	protected float carryInertiaY(float moveY)
	{
		// Nothing happens with blocks you are horizontally colliding with yet
		// If the object carrying you (above/below) could push you
		if (objectStopY != null && pushPriority < objectStopY.pushPriority)
		{
			// Let it move you if you are moving in opposite directions
			// This is so that you don't move if it is already pushing you
			if (objectStopY.totalYSpeed > 0 != moveY < 0)
				return moveY+objectStopY.totalYSpeed;
		}
		return moveY;
	}
	
	/**
	 * {@inheritDoc}
	 */
	protected float doMoveTo(Room room,float targetPos, boolean dimension)
	{	
		// Calculate the direction you are moving in
		// TRUE -> left/up, FALSE -> right/down
		boolean direction = targetPos<(dimension?x:y);
		
		// Find out how close you can move
		float returnPos = tryMoveTo(room,targetPos, direction, dimension, pushPriority);
		// Set the collision flags
		if (scaledFloat(returnPos) != scaledFloat(targetPos))
		{
			if (dimension)
			{
				horizStop[direction?0:1] = true;
				objectStopX = lastObjectStop;
			}
			else
			{
				vertStop[direction?0:1] = true;
				objectStopY = lastObjectStop;
			}
		}
		return returnPos;
	}
	
	/**
	 * A method that tries to move the object to a given position in
	 * a certain dimension.
	 * 
	 * @param targetPos - The position to try to move to.
	 * @param direction - The direction being moved in. (TRUE -> left/up, FALSE -> right/down)
	 * @param dimension - The dimension being moved in (TRUE -> X, FALSE -> Y)
	 * @param callerPriority - The priority of the pushing object (it's possible the object is being pushed by something else.)
	 * @return The actual position that can be moved to.
	 */
	private float tryMoveTo(Room room,float targetPos, boolean direction, boolean dimension, int callerPriority)
	{
		// Reset the object collision flag
		lastObjectStop = null;
		
		// Calculate the edges of the bounding box between
		// where you are and where you're trying to move
		float boxBound[][] = getMovementBox(dimension?targetPos:x,dimension?y:targetPos,direction);

		/** This object has yet to be replaced by other generic environment collision code. */
		
		// REMOVE AND REPLACE WITH ACTUAL ENVIRONMENT COLLISION BEHAVIOUR LATER
		// IT ONLY STOPS YOU FROM GOING OFF-SCREEN RIGHT NOW WHICH MAY NOT EVEN BE WHAT WE WANT
		// JEEZ DUCKO, LEAVING HIDDEN CONSTANTS EVERYWHERE. IT TAKES AGES TO DEBUG
		// WHEN YOU DO THAT, YOU KNOW
		// If you hit the negative boundary
		if (targetPos < 0)
		{
			targetPos = 0;
		}
		// If you hit the positive boundary
		if(dimension)
		{
			if(targetPos>room.getWidth()-width)
				targetPos=room.getWidth()-width;
		}
		else
		{
			if(targetPos>room.getHeight()-height)
				targetPos=room.getHeight()-height;
		}
		
		/** This code checks for all nearby objects to see if it would have to move through any of them
		 * and it determines the farthest it can move before either getting stopped by something.
		 */
		// Go through every tile
		for (int i = locationToTile(boxBound[0][0]); i <= locationToTileUp(boxBound[0][1]); i++)
		{
			for (int j = locationToTile(boxBound[1][0]); j <= locationToTileUp(boxBound[1][1]); j++)
			{
				// Get the list of objects overlapping this tile
				HashSet<Collidable> overlapList = room.getSpaceHash().getObjects(i,j);
				// If you picked an invalid list, do nothing
				if (overlapList == null) continue;
				// Otherwise turn the list into an array (we can't use the iterator because of improper concurrency protection)
				Collidable overlaps[] = new Collidable[overlapList.size()];
				overlaps = overlapList.toArray(overlaps);
				
				// Go through every object in the list
				for (int k = 0; k < overlaps.length; k++)
				{
					Collidable curObj = overlaps[k];
					// If you haven't already checked collision for this object and the rectangles of the two are overlapping
					if (!alreadyChecked.contains(curObj) &&
						boxBound[0][0] < curObj.x+curObj.width && curObj.x < boxBound[0][1] &&
						boxBound[1][0] < curObj.y+curObj.height && curObj.y < boxBound[1][1])
					{
						// Set this object to having been checked
						alreadyChecked.add(curObj);
						// Calculate how far out the block should move
						targetPos = tryMoveToObj(room,curObj, targetPos, direction, dimension, callerPriority);
					}
				}
			}
		}
		// Clear the list of objects already checked
		alreadyChecked.clear();
		
		return scaledFloat(targetPos)/GRANULARITY;
	}
	
	public float scaledFloat(float a)
	{
		return (int)(a*GRANULARITY);
	}
	
	/**
	 * Calculates the bounding box to check from your location to the given one.
	 * 
	 * It's worth noting that only one target location should
	 * be relevant at any given time, which is why only
	 * one direction parameter is given.
	 * 
	 * @param targetX - The target X location.
	 * @param targetY - The target Y location.
	 * @param direction - The direction being moved in. (TRUE -> left/up, FALSE -> right/down)
	 * @return Returns an array containing the bounding box.
	 */
	private float[][] getMovementBox(float targetX, float targetY, boolean direction)
	{
		float boxBound[][] = new float[2][2];
		
		// Find the bounding box depending on the movement targets given and the direction moving in
		boxBound[0][0] = direction?targetX:x;
		boxBound[0][1] = direction?x+width:targetX+width;
		boxBound[1][0] = direction?targetY:y;
		boxBound[1][1] = direction?y+height:targetY+height;
		
		return boxBound;
	}
	
	/**
	 * Translates the location of the object to a space hash tile.
	 * Accounts for weird anomalies involving negative integer truncation.
	 * 
	 * @param realPos - The actual location to translate.
	 * @return - The translated tile location.
	 */
	private int locationToTile(float realPos)
	{
		return (int)Math.floor(realPos/16);
	}
	
	private int locationToTileUp(float realPos)
	{
		return (int)Math.floor((realPos+0.0001)/16);
	}
	
	/**
	 * If you are attempting to move into an object, this function calculates
	 * a new position for you so that you don't.
	 * 
	 * @param curObj - The object being moved into.
	 * @param targetPos - The position you are trying to move to. 
	 * @param direction - The direction being moved in. (TRUE -> left/up, FALSE -> right/down)
	 * @param dimension - The dimension being moved in (TRUE -> X, FALSE -> Y)
	 * @param callerPriority - The priority of the pushing object.
	 */
	private float tryMoveToObj(Room room, Collidable curObj, float targetPos, boolean direction, boolean dimension, int callerPriority)
	{
		/** If you can push the object, try to calculate how far you could theoretically push it.
		 * This recursive call initially seems expensive, but the chances of an object needing to push
		 * through a large stack of other objects is not all that likely.
		 */
		
		float pushPos;
		// If you are allowed to push the other object out of the way
		//if (callerPriority > curObj.pushPriority)
			// Calculate how far you can push it
			//pushPos = tryMoveThruObj(room, curObj, targetPos, direction, dimension, callerPriority);
		// Otherwise, just use its actual position
		//else
			pushPos = dimension?curObj.x:curObj.y;
		
		/** Knowing the farthest the object can be, calculate the farthest you can be. */
		
		// Calculate how far the object pushes you back to.
		float backPushPos;
		if (direction) backPushPos = dimension?pushPos+curObj.width:pushPos+curObj.height;
		else backPushPos = dimension?pushPos-width:pushPos-height;
		
		// If this pushes you back to a less favourable position
		if ((direction && (backPushPos > targetPos)) ||
			(!direction && (backPushPos < targetPos)))
		{
			// Use this location instead
			targetPos = backPushPos;
			// Mark the object that pushed you
			lastObjectStop = curObj;
		}
		
		return targetPos;
	}
	
	/**
	 * If you are attempting to move into an object, push that object out of the way.
	 * 
	 * @param curObj - The object being moved into.
	 * @param targetPos - The position you are trying to move to. 
	 * @param direction - The direction being moved in. (TRUE -> left/up, FALSE -> right/down)
	 * @param dimension - The dimension being moved in (TRUE -> X, FALSE -> Y)
	 * @param callerPriority - The priority of the pushing object.
	 */
	private float tryMoveThruObj(Room room, Collidable curObj, float targetPos, boolean direction, boolean dimension, int callerPriority)
	{
		// Calculate the position to try to move the object to
		float tryPushPos;
		if (direction) tryPushPos = dimension?targetPos-curObj.width:targetPos-curObj.height;
		else tryPushPos = dimension?targetPos+width:targetPos+height;
		
		// Remove the object from the hash so it can be moved
		curObj.removeHash(room);
		// Try pushing the object
		float pushPos = curObj.tryMoveTo(room, tryPushPos, direction, dimension, callerPriority);
		// Relocate it
		if (dimension) curObj.x = pushPos;
		else curObj.y = pushPos;
		// Place the object back into the hash
		curObj.addHash(room);
		
		return pushPos;
	}
	
	/**
	 * Adds references to the tiles being overlapped into the spatial hash.
	 */
	private void addHash(Room room)
	{	
		// Calculate the bounding box of the tiles the object overlaps
		int leftPos = (int)(x/SpaceHash.HASH_SIZE), rightPos = (int)((x+width)/SpaceHash.HASH_SIZE);
		int topPos =(int)(y/SpaceHash.HASH_SIZE), botPos = (int)((y+height)/SpaceHash.HASH_SIZE);
		// Go through every tile and add a reference to the list
		for (int i = leftPos; i <= rightPos; i++)
		{
			for (int j = topPos; j <= botPos; j++)
			{
				room.getSpaceHash().addCollidable(i,j,this);
			}
		}
	}
	
	/**
	 * Removes references to the tiles being overlapped into the spatial hash.
	 */
	private void removeHash(Room room)
	{
		// Calculate the bounding box of the tiles the object overlaps
		int leftPos = (int)(x/SpaceHash.HASH_SIZE), rightPos = (int)((x+width)/SpaceHash.HASH_SIZE);
		int topPos =(int)(y/SpaceHash.HASH_SIZE), botPos = (int)((y+height)/SpaceHash.HASH_SIZE);
		// Go through every tile and remove a reference from the list
		for (int i = leftPos; i <= rightPos; i++)
		{
			for (int j = topPos; j <= botPos; j++)
			{
				room.getSpaceHash().removeCollidable(i,j,this);
			}
		}
	}
}
